/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.spring.web.bind;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.vacoor.nothing.common.util.Castor;
import org.vacoor.nothing.common.util.StringUtils;
import org.vacoor.nothing.web.domain.Filters;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;

/**
 * 动态过滤条件参数解析器
 * 支持以下形式: search_field, search_field_operator, search_field_type, search_field_operator_type
 * eg:
 * 年龄小于100则参数形式为: search_age_LT_I = 100  (search_字段名_操作符_类型 = 值)
 * 年龄等于10或20则参数为: search_age_I = 10 & search_age_I = 20 (操作符默认为=)
 * <p/>
 * 该解析器额外提供通过 {@link org.springframework.beans.factory.annotation.Qualifier}
 * 来限定每个{@link org.vacoor.nothing.web.domain.Filters} 解析的 domain
 * {@link org.vacoor.nothing.web.domain.Filters} 解析器
 * <p/>
 * TODO 完成另外两种:
 * extJS grid filter:
 * filter[0][field]:name            前缀 组标识 值类型
 * filter[0][data][type]:numeric    前缀 组标识 。。
 * filter[0][data][comparison]:eq
 * filter[0][data][value]:2
 * json:
 * {
 * property:
 * value:
 * operator:
 * fieldType:
 * }
 *
 * @author vacoor
 */
@SuppressWarnings({"unused"})
public class FiltersHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    /**
     * 字段类型
     */
    public enum FieldType {
        B,  /* Boolean */
        I,  /* Integer */
        L,  /* Long */
        F,  /* Float */
        N,  /* Double */
        S,  /* String */
        D;  /*Date*/

        public FieldType forName(String name) {
            try {
                return valueOf(name.toUpperCase());
            } catch (Exception e) {
                return null;
            }
        }
    }

    private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
    private static final String DEFAULT_PREFIX_PREFIX = "search_";
    private static final char DEFAULT_EXPRESSION_DELIMITER = '_';
    private static final String DEFAULT_PARAMETER_MAPPING_DELIMITER = ":";

    private static final Filters.Operator DEFAULT_FALLBACK_SEARCH_OPERATOR = Filters.Operator.EQ;
    private static final FieldType DEFAULT_FALLBACK_SEARCH_TYPE = FieldType.S;

    private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
    private String filterPrefix = DEFAULT_PREFIX_PREFIX;
    private char expressionDelimiter = DEFAULT_EXPRESSION_DELIMITER;
    private String parameterTransformDelimiter = DEFAULT_PARAMETER_MAPPING_DELIMITER;

    private Filters.Operator fallbackOperator = DEFAULT_FALLBACK_SEARCH_OPERATOR;
    private FieldType fallbackFieldType = DEFAULT_FALLBACK_SEARCH_TYPE;


    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return Filters.class.equals(parameter.getParameterType());
    }

    @Override
    public Filters resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        FiltersConfig settings = getSearchKeywordSetting(parameter);

        Map<String, String[]> paramMap = webRequest.getParameterMap();
        String prefix = filterPrefix + getQualifierPrefix(parameter);
        List<String> allowList = settings != null ? Lists.newArrayList(settings.allow()) : null;
        List<String> denyList = settings != null ? Lists.newArrayList(settings.deny()) : null;
        Map<String, String> transformMap = settings != null ? parseParameterMapping(settings.transform()) : null;
        String[] dateFormats = settings != null ? settings.dateFormats() : null;

        Filters filters = Filters.create();
        int prefixLen = prefix.length();
        for (Map.Entry<String, String[]> param : paramMap.entrySet()) {
            String name = param.getKey();
            String[] values = StringUtils.clean(param.getValue());

            // values is empty or or param name less prefix length or not starts width prefix contains empty
            // if (values.length < 1 || name.length() < prefixLen + 1 || !name.startsWith(prefix) || name.matches(".*\\s+.*")) {
            // UPDATE 2015.01.17 NU/NN 操作符值为""
            if (name.length() < prefixLen + 1 || !name.startsWith(prefix) || name.matches(".*\\s+.*")) {
                continue;
            }

            StringBuilder expr = new StringBuilder(name);
            expr.delete(0, prefixLen);  // delete prefix

            FieldType fieldType = null;
            Filters.Operator operator = null;
            int length = expr.length();
            // property_op_t
            if (length > 2 && expressionDelimiter == expr.charAt(length - 2)) {
                fieldType = FieldType.valueOf(expr.substring(length - 1));
                // property_op_t --> property_op
                expr.delete(length - 2, length);
                length = expr.length();
            }

            // property_op
            if (length > 3 && expressionDelimiter == expr.charAt(length - 3)) {
                try {
                    operator = Filters.Operator.forName(expr.substring(length - 2));
                    // property_op --> property
                    expr.delete(length - 3, length);
                } catch (IllegalArgumentException ignore) {
                }
            }

            // UPDATE 2015.01.17 值为"" 但是不是空操作符
            if (values.length < 1 && Filters.Operator.NU != operator && Filters.Operator.NN != operator) {
                continue;
            }

            name = expr.toString();
            fieldType = fieldType != null ? fieldType : fallbackFieldType;
            operator = operator != null ? operator : fallbackOperator;

            if (denyList != null && denyList.size() > 0 && denyList.contains(name)) {
                continue;
            }

            if (allowList != null && allowList.size() > 0 && !allowList.contains(name)) {
                continue;
            }

            if (transformMap != null) {
                String s = transformMap.get(name);
                name = s != null ? s : name;
            }

            filters.add(name, operator, ensureType(values, fieldType, operator, dateFormats));
        }

        return filters;
    }

    private String getQualifierPrefix(MethodParameter parameter) {
        StringBuilder buffer = new StringBuilder();
        Qualifier qualifier;
        if (parameter != null && (qualifier = parameter.getParameterAnnotation(Qualifier.class)) != null) {
            buffer.append(qualifier.value());
            // buffer.append(qualifierDelimiter);
        }
        return buffer.toString();
    }

    private FiltersConfig getSearchKeywordSetting(MethodParameter parameter) {
        FiltersConfig settings = parameter.getParameterAnnotation(FiltersConfig.class);
        settings = settings != null ? settings : parameter.getMethodAnnotation(FiltersConfig.class);

        return settings;
    }

    private Map<String, String> parseParameterMapping(String[] mapping) {
        Map<String, String> parameterMapping = Maps.newLinkedHashMap();
        for (String string : mapping) {
            String[] split = string.split(parameterTransformDelimiter);
            split = StringUtils.clean(split);

            if (2 != split.length) {
                throw new IllegalStateException("search keyword parameter transform: " + string);
            }

            parameterMapping.put(split[0], split[1]);
        }

        return parameterMapping;
    }

    private Object[] ensureType(Object[] values, FieldType fieldType, Filters.Operator operator, String... dateFormats) {
        fieldType = Filters.Operator.LK != operator ? fieldType : FieldType.S;
        Object[] converted = new Object[values.length];

        for (int i = 0; i < values.length; i++) {
            switch (fieldType) {
                case B:
                    converted[i] = Castor.asBoolean(values[i]);
                    break;
                case I:
                    converted[i] = Castor.asInt(values[i]);
                    break;
                case L:
                    converted[i] = Castor.asLong(values[i]);
                    break;
                case F:
                    converted[i] = Castor.asFloat(values[i]);
                    break;
                case N:
                    converted[i] = Castor.asDouble(values[i]);
                    break;
                case D:
                    if (dateFormats != null) {
                        String text = Castor.asString(values[i]);
                        for (String format : dateFormats) {
                            try {
                                converted[i] = new SimpleDateFormat(format).parse(text);
                                break;
                            } catch (ParseException ignore) {
                            }
                        }
                    }
                    converted[i] = converted[i] != null ? converted[i] : Castor.asDate(values[i]);
                    break;
                case S:
                default:
                    converted[i] = Castor.asString(values[i]);
                    // converted[i] = Filters.Operator.LK != operator ? converted[i] : ("%" + converted[i] + "%");
                    // UPDATE 2015.01.17 更新,参考 jqGrid
                    converted[i] = operator.wrapValueIfNecessary(values[i]);
            }
        }

        return converted;
    }
}
